import echarts from 'echarts'
import moment from 'moment'
import { enums, fromValue as val2Enum } from 'src/assets/js/enums'
import { MomentJsUtils } from '../../assets/js/util'

const secPerMin = 60
const minPerHour = 60
const aSec = 1_000
const aMin = secPerMin * aSec
const anHour = minPerHour * aMin

/**
 * 机台状态图表
 */
export class MachineStatsChart {
  /**
   * ECharts 配置对象
   *
   * @type {{}}
   * @private
   */
  _option = {}

  /**
   * ECharts 实例
   *
   * @type {{}}
   * @private
   */
  _echartsInstance

  /**
   * 各机台状态的总持续时间
   * <p>
   * key 为 {@link _echartsDatumMetadata} 的 key，value 为持续的总毫秒数
   *
   * @type {{}}
   * @private
   */
  _statTotalDurations = {}

  /**
   * 构建一个机台状态图表
   *
   * @param {{}} echartsInstance ECharts 实例
   */
  constructor (echartsInstance) {
    this._echartsInstance = echartsInstance
    this._option = {
      dataZoom: this._dataZoom(),
      xAxis: this._xAxis(),
      yAxis: this._yAxis(),
      series: this._series(),
      grid: this._grid(),
      tooltip: this._tooltip(),
      visualMap: this._visualMap()
    }

    // 计算 visual map 的 item 间间隔
    this._resetVisualMapItemGap()
  }

  /**
   * 获取 ECharts 实例
   * <p>
   * 为保证可扩展性，开放本方法用于直接获取 ECharts 实例
   *
   * @returns {{}}
   */
  getInstance () {
    return this._echartsInstance
  }

  /**
   * 设置班次起止时间
   * <p>
   * 设置后将影响 X 轴的表现，from 为 X 轴最小值、to 为 X 轴最大值
   *
   * @param {Date} from 班次开始时间
   * @param {Date} to 班次结束时间
   * @return {MachineStatsChart} 返回自身，便于链式调用
   */
  setSchedulingTime (from, to) {
    const xAxis = this._option.xAxis

    xAxis.min = from.getTime()
    xAxis.max = to.getTime()

    // 计算班制持续时间（小时），不足一小时的部分向上取整到一小时
    // e.g., 班制持续 2.5h 时，视为 3h
    const minutes = moment(to).diff(moment(from), 'minutes')
    // 期望分隔段数 = 班制持续小时数，i.e., 班制有几个小时，X 轴就分成几段
    xAxis.splitNumber = Math.ceil(minutes / minPerHour)

    return this
  }

  /**
   * 一条来自 API 的机台状态数据
   *
   * @typedef {{type: number, from: string, to: string}} MachineStatDatum
   * @property {number} type 机台状态枚举值
   * @property {string} from 该状态的开始时间，符合 ISO 8601 的时间字符串
   * @property {string} to 该状态的结束时间，符合 ISO 8601 的时间字符串
   */

  /**
   * 一条来自 API 的休息数据
   *
   * @typedef {{from: string, to: string, description: string}} RestDatum
   * @property {string} from 休息开始时间，符合 ISO 8601 的时间字符串
   * @property {string} to 休息结束时间，符合 ISO 8601 的时间字符串
   * @property {string} description 对休息的描述，e.g. 午休
   */

  /**
   * 设置图表的数据集
   * <p>
   * 本方法接受 API 返回的原始数据集，内部对原始数据进行转换得到 ECharts 数据集
   * （相比原始数据进行了结构重组、扩展了一些用于 ECharts 的内容），并覆盖进 ECharts 配置项中
   * <p>
   * 若空参数调用，则会清空已有数据
   *
   * @param {RestDatum[]} [rests] 休息数据集（取自 API）
   * @param {MachineStatDatum[]} [stats] 机台状态数据集（取自 API）
   * @returns {MachineStatsChart} 返回自身，便于链式调用
   */
  setData ({ rests, stats }) {
    const newData = []

    // 休息数据
    if (rests) {
      newData.push(...rests.map(MachineStatsChart._parseRestDatum))
    }

    // 机台状态数据
    if (stats) {
      const parsedStats = stats.map(MachineStatsChart._parseMachineStatDatum)
      this._recalculateStatTotalDurations(parsedStats)
      newData.push(...parsedStats)
    }

    // Y 轴的背景 item
    newData.push(...MachineStatsChart._generateBackgroundEchartsData())

    // 替换已有数据
    const series = Array.isArray(this._option.series) ? this._option.series[0] : this._option.series
    series.data = newData

    return this
  }

  /**
   * 重算各机台状态的总持续时长
   *
   * @param {[{}]} parsedMachineStats 经 {@link _parseMachineStatDatum} 解析后的机台状态数据数组
   * @private
   */
  _recalculateStatTotalDurations (parsedMachineStats) {
    const newDurations = {}

    for (const stat of parsedMachineStats) {
      const from = stat.value[0]
      const to = stat.value[1]
      const metadataKey = stat.value[3]

      if (newDurations[metadataKey] === undefined) {
        newDurations[metadataKey] = 0
      }

      newDurations[metadataKey] += to.getTime() - from.getTime()
    }

    this._statTotalDurations = newDurations
  }

  /**
   * 重算图表中各项的尺寸
   * <p>
   * 当视图大小发生变化时，应手动调用本方法
   *
   * @returns {MachineStatsChart} 返回自身，便于链式调用
   */
  resize () {
    this._echartsInstance.resize()
    this._resetVisualMapItemGap()
    return this
  }

  /**
   * 重新渲染 ECharts
   * <p>
   * 应作为链式调用的末尾使用，调用后将使修改的配置（e.g. 图表数据、班次时间等）生效
   *
   * @see https://echarts.apache.org/zh/api.html#echartsInstance.setOption
   */
  rerender () {
    this._echartsInstance.clear()
    this._echartsInstance.setOption(this._option)
  }

  /**
   * 根据当前 ECharts 容器的宽度重新计算 VisualMap 中每一项的间距
   *
   * @private
   */
  _resetVisualMapItemGap () {
    const visualMap = Array.isArray(this._option.visualMap) ? this._option.visualMap[0]
      : this._option.visualMap

    const totalWidth = this._echartsInstance.getWidth()
    const itemWidth = 170
    const margin = 20
    const itemCount = visualMap.categories.length

    if (totalWidth === 0) {
      return
    }

    const blankWidth = totalWidth - itemWidth * itemCount

    if (blankWidth <= 0) {
      return
    }

    visualMap.itemGap = blankWidth / (itemCount - 1) + margin
  }

  /**
   * ECharts 数据的元数据，用于描述这些 ECharts 数据在图表中的表现形式
   *
   * @typedef {{}} EChartsDatumMetadata
   * @property {string} name 元数据的名称，与 {@link _echartsDatumMetadata} 的字段名一致，可作为
   * {@link _echartsDatumMetadata} 的 key 使用
   * @property {'status','rest','background'} type 表示这类 ECharts 数据的类型
   * <p>
   * 元数据本身已经对各式 ECharts 数据进行了分类，type 在这基础上进行更抽象的分类
   * <p>
   * 原始数据中，机台状态数据对应的 type 是 "status"；休息数据对应的 type 是 "rest"
   * <p>
   * "background" 没有与之对应的原始数据，"background" 类的 ECharts 数据是图表中每一行的浅灰色背景，
   * 这些浅灰色背景是通过一条占满 X 轴的浅灰色数据条实现的，这些数据条也是一类 ECharts 数据，是为
   * "background"
   * @property {number} category 表示该 ECharts 数据对应图表 Y 轴中的 category 索引
   * <p>
   * Y 轴从上到下的三个索引依次为 0、1、2
   * @property {number} heightPercent 表示该 ECharts 数据的高度百分比
   * <p>
   * 1 = 全高，0 = 不显示
   * @property {number} z 表示该 ECharts 数据的渲染层次
   * <p>
   * 当数据条发生重叠时，高 z 值的数据条将盖住低 z 值的数据条
   * @property {boolean} silent 指示该类 ECharts 数据条是否响应和触发鼠标事件
   * <p>
   * 设为 true 时，该 ECharts 数据条无法发生交互，包括 tooltip 等组件都将无法触发
   * @property {'top','topCenter','center','bottomCenter','bottom'} vAlign 指示数据条的垂直对齐方式
   * <p>
   * 所有数据条都有一个固定的标准高度，该高度可能小于 Y 轴分隔区域的高度，vAlign
   * 属性可以指定数据条在分隔区域内的垂直对齐方式
   * <p>
   * "top"、"center"、"bottom" 无需赘述，"topCenter"、"bottomCenter" 的意义在于，
   * 某些数据条可能按标准高度显示，某些数据条可能指定了 heightPercent 属性而小于标准高度，
   * 此时如果希望标准高度的数据条居上、小于标准高度的数据条居于标准高度数据条的中央，就需要指定
   * vAlign 为 "topCenter"
   */

  /**
   * ECharts 数据的元数据
   *
   * @type {{EChartsDatumMetadata}}
   * @private
   */
  static _echartsDatumMetadata = {
    halting: {
      name: undefined,
      type: 'status',
      category: 0,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    machining: {
      name: undefined,
      type: 'status',
      category: 0,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    offline: {
      name: undefined,
      type: 'status',
      category: 0,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    disconnecting: {
      name: undefined,
      type: 'status',
      category: 0,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    warning: {
      name: undefined,
      type: 'status',
      category: 0,
      heightPercent: 0.7,
      z: 10,
      silent: false,
      vAlign: 'bottomCenter'
    },
    changeover: {
      name: undefined,
      type: 'status',
      category: 1,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    breakdown: {
      name: undefined,
      type: 'status',
      category: 1,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    repair: {
      name: undefined,
      type: 'status',
      category: 1,
      heightPercent: 1,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    rest: {
      name: undefined,
      type: 'rest',
      category: 2,
      heightPercent: 0.25,
      z: 5,
      silent: false,
      vAlign: 'bottom'
    },
    background0: {
      name: undefined,
      type: 'background',
      category: 0,
      heightPercent: 1,
      z: 2,
      silent: true,
      vAlign: 'bottom'
    },
    background1: {
      name: undefined,
      type: 'background',
      category: 1,
      heightPercent: 1,
      z: 2,
      silent: true,
      vAlign: 'bottom'
    }
  }

  // 向 _echartsDatumMetadata 对象的每一项中，插入该项的字段名
  static _iife = (() => {
    Object.entries(MachineStatsChart._echartsDatumMetadata).forEach(entry => {
      entry[1].name = entry[0]
    })
  })()

  /**
   * 机台状态枚举值 - ECharts 数据元数据 key 驱动表
   *
   * @type {EChartsDatumMetadata[]}
   * @private
   */
  static _statMetadataKey = (() => {
    const arr = []
    const mEnums = enums.MACHINES_STATUS
    const metadata = MachineStatsChart._echartsDatumMetadata

    arr[mEnums.HALTING.value] = metadata.halting.name
    arr[mEnums.MACHINING.value] = metadata.machining.name
    arr[mEnums.OFFLINE.value] = metadata.offline.name
    arr[mEnums.DISCONNECTING.value] = metadata.disconnecting.name
    arr[mEnums.WARNING.value] = metadata.warning.name
    arr[mEnums.CHANGEOVER.value] = metadata.changeover.name
    arr[mEnums.BREAKDOWN.value] = metadata.breakdown.name
    arr[mEnums.REPAIR.value] = metadata.repair.name

    return arr
  })()

  /**
   * ECharts 数据元数据 key - 机台状态枚举驱动表
   *
   * @type {{}}
   * @private
   */
  static _metadataKeyStat = (() => {
    const map = {}

    MachineStatsChart._statMetadataKey.forEach((key, enumVal) => {
      map[key] = val2Enum(enums.MACHINES_STATUS, enumVal)
    })

    return map
  })()

  /**
   * 用于 ECharts custom series 的渲染函数
   * <p>
   * ECharts 数据集中的每一项数据都会通过此函数进行渲染
   *
   * @param {{}} ctx 当前 ECharts 图表上下文信息
   * @param {{}} api 协助渲染的工具方法集
   * @private
   * @see https://echarts.apache.org/zh/option.html#series-custom.renderItem
   */
  static _renderItem (ctx, api) {
    const from = api.value('from')
    const to = api.value('to')
    const category = api.value('category')
    const metadataKey = api.value('metadataKey')
    /** @type EChartsDatumMetadata */
    const metadata = MachineStatsChart._echartsDatumMetadata[metadataKey]

    // Y 轴分隔区域的高度
    const areaHeight = api.size([0, 1])[1]
    // 数据条标准高度
    const fullBarHeight = 32

    const fromPoint = api.coord([from, category])
    let barHeight = fullBarHeight * metadata.heightPercent

    // 根据 vAlign 计算起始点的 Y 轴坐标值
    let fromY
    switch (metadata.vAlign) {
      case 'top':
        fromY = fromPoint[1] - areaHeight / 2
        break
      case 'topCenter':
        fromY = fromPoint[1] - areaHeight / 2 + (fullBarHeight - barHeight) / 2
        break
      case 'center':
        fromY = fromPoint[1] - barHeight / 2
        break
      case 'bottom':
        fromY = fromPoint[1] + areaHeight / 2 - barHeight
        break
      case 'bottomCenter':
        const bottomY = fromPoint[1] + areaHeight / 2
        fromY = bottomY - (fullBarHeight - barHeight) / 2 - barHeight
        break
      default:
        break
    }

    // 根据 ECharts 数据的类型计算起始点的 X 轴坐标、数据条宽度
    let barWidth
    let fromX
    switch (metadata.type) {
      case 'status':
      case 'rest':
        const toPoint = api.coord([to, category])
        barWidth = toPoint[0] - fromPoint[0]
        fromX = fromPoint[0]
        break

      case 'background':
        barWidth = ctx.coordSys.width
        fromX = ctx.coordSys.x
        break
      default:
        break
    }

    // 将数据条裁剪到坐标轴区域内
    const rectShape = echarts.graphic.clipRectByRect(
      {
        x: fromX,
        y: fromY,
        width: barWidth,
        height: barHeight
      },
      {
        x: ctx.coordSys.x,
        y: ctx.coordSys.y,
        width: ctx.coordSys.width,
        height: ctx.coordSys.height
      }
    )

    return rectShape && {
      type: 'rect',
      shape: rectShape,
      style: api.style(),
      z2: metadata.z,
      silent: metadata.silent
    }
  }

  /**
   * 解析一条机台状态数据（取自 API 的原始数据），转换为一条 ECharts 数据并返回
   *
   * @param {{}} stat 一条机台状态数据
   * @return {{}} 一条 ECharts 数据
   */
  static _parseMachineStatDatum (stat) {
    const from = new Date(stat.from)
    const to = new Date(stat.to)
    const metadataKey = MachineStatsChart._statMetadataKey[stat.type]
    const metadata = MachineStatsChart._echartsDatumMetadata[metadataKey]
    const statEnum = val2Enum(enums.MACHINES_STATUS, stat.type)

    return {
      name: statEnum.desc,
      value: [
        from,
        to,
        metadata.category,
        metadataKey
      ]
    }
  }

  /**
   * 解析一条休息数据（取自 API 的原始数据），转换为一条 ECharts 数据并返回
   *
   * @param {{}} rest 一条休息数据
   * @return {{}} 一条 ECharts 数据
   */
  static _parseRestDatum (rest) {
    const from = new Date(rest.from)
    const to = new Date(rest.to)
    /** @type {EChartsDatumMetadata} */
    const metadata = MachineStatsChart._echartsDatumMetadata.rest

    return {
      name: rest.description,
      value: [
        from,
        to,
        metadata.category,
        metadata.name
      ],
      visualMap: false,
      itemStyle: {
        color: 'transparent',
        borderColor: '#F7B82D',
        borderType: 'dashed'
      }
    }
  }

  /**
   * 生成用于绘制 Y 轴上每一行的浅灰色背景的 ECharts 数据集
   *
   * @return {{}[]} ECharts 数据集
   */
  static _generateBackgroundEchartsData () {
    /** @type {EChartsDatumMetadata} */
    const firstMetadata = MachineStatsChart._echartsDatumMetadata.background0
    /** @type {EChartsDatumMetadata} */
    const secondMetadata = MachineStatsChart._echartsDatumMetadata.background1

    return [
      {
        value: [null, null, firstMetadata.category, firstMetadata.name],
        visualMap: false,
        itemStyle: {
          color: '#F3F3F3'
        }
      },
      {
        value: [null, null, secondMetadata.category, secondMetadata.name],
        visualMap: false,
        itemStyle: {
          color: '#F3F3F3'
        }
      }
    ]
  }

  /**
   * 绘图网格 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#grid
   */
  _grid () {
    return {
      top: 0,
      right: 20,
      bottom: 80,
      left: 20,
      containLabel: true
    }
  }

  /**
   * 缩放 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#dataZoom
   */
  _dataZoom () {
    return {
      type: 'inside',
      minValueSpan: anHour,
      disabled: true,
      throttle: 100
    }
  }
  _changeZoom (val) {
    this._option.dataZoom.disabled = val
    return this
  }
  /**
   * X 轴 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#xAxis
   */
  _xAxis () {
    return {
      type: 'time',
      minInterval: aSec,
      maxInterval: anHour,
      axisLine: { lineStyle: { color: '#D3D3D3' } },
      axisTick: {
        inside: true,
        length: 8
      },
      minorTick: {
        show: true,
        splitNumber: 6
      },
      axisLabel: {
        formatter: value => moment(value).format('HH:mm'),
        showMinLabel: true,
        showMaxLabel: true,
        color: '#333'
      },
      splitLine: { show: false },
      axisPointer: {
        show: true,
        triggerTooltip: false
      }
    }
  }

  /**
   * Y 轴 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#yAxis
   */
  _yAxis () {
    return {
      show: false,
      type: 'category',
      inverse: true,
      axisLine: { show: false },
      axisTick: { show: false },
      axisLabel: { show: false },
      data: ['line 1', 'line 2', 'line 3']
    }
  }

  /**
   * 系列 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#series
   */
  _series () {
    return {
      name: 'stats_series',
      type: 'custom',
      coordinateSystem: 'cartesian2d',
      encode: {
        x: -1,
        y: 'category'
      },
      renderItem: MachineStatsChart._renderItem,
      data: [],
      dimensions: [
        { name: 'from', type: 'time' },
        { name: 'to', type: 'time' },
        'category',
        'metadataKey'
      ]
    }
  }

  /**
   * 提示 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#tooltip
   */
  _tooltip () {
    return {
      show: true,
      trigger: 'item',
      backgroundColor: 'rgba(0, 0, 0, .4)',
      formatter: params => {
        const from = params.value[0]
        const to = params.value[1]
        const metadataKey = params.value[3]
        /** @type EChartsDatumMetadata */
        const metadata = MachineStatsChart._echartsDatumMetadata[metadataKey]

        let durationText
        if (from && to) {
          const duration = moment.duration(moment(to).diff(moment(from)))
          durationText = MomentJsUtils.humanizeDurationPrecisely(duration)
        }

        switch (metadata.type) {
          case 'rest':
            return `${params.name}: ${durationText}`
          case 'status':
            return `${params.marker}${params.name}: ${durationText}`
          default:
            return ''
        }
      }
    }
  }

  /**
   * 视觉映射 option
   *
   * @type {{}}
   * @private
   * @see https://echarts.apache.org/zh/option.html#visualMap
   */
  _visualMap () {
    const metadata = MachineStatsChart._echartsDatumMetadata

    const categories = [
      metadata.machining.name,
      metadata.halting.name,
      metadata.disconnecting.name,
      metadata.offline.name,
      metadata.warning.name,
      metadata.changeover.name,
      metadata.breakdown.name,
      metadata.repair.name
    ]

    const inRangeColors = {}
    categories.forEach(metadataKey => {
      inRangeColors[metadataKey] = MachineStatsChart._metadataKeyStat[metadataKey].color
    })

    return {
      type: 'piecewise',
      categories: categories,
      dimension: 3,
      inRange: {
        symbol: 'circle',
        color: inRangeColors
      },
      itemWidth: 10,
      itemHeight: 10,
      orient: 'horizontal',
      textStyle: {
        fontSize: 14
      },
      bottom: 40,
      left: 15,
      itemGap: 0,
      formatter: metadataKey => {
        const durationInMs = this._statTotalDurations[metadataKey] || 0
        const duration = moment.duration(durationInMs, 'milliseconds')
        const durationText = MomentJsUtils.humanizeDurationPrecisely(duration, 's')
        // console.log(metadataKey, durationInMs, duration, durationText)
        return `${MachineStatsChart._metadataKeyStat[metadataKey].desc}: ${durationText}`
      }
    }
  }
}
